package com.tuuzed.android.sqlite;


import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;

import com.tuuzed.android.sqlite.args.CountArgs;
import com.tuuzed.android.sqlite.args.DatabaseArgs;
import com.tuuzed.android.sqlite.args.FindArgs;
import com.tuuzed.android.sqlite.internal.ColumnInfo;
import com.tuuzed.android.sqlite.internal.SQLiteValueConverter;
import com.tuuzed.android.sqlite.internal.SqlUtil;
import com.tuuzed.android.sqlite.internal.StringUtil;
import com.tuuzed.android.sqlite.internal.TableInfo;
import com.tuuzed.android.sqlite.internal.TableInfos;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

class SQLiteImpl implements SQLite {
    private static final String TAG = "SQLite";

    private SQLiteOpenHelper mDatabaseHelper;
    private SQLiteDatabase mWritableDatabase;
    private SQLiteDatabase mReadableDatabase;
    private SQLiteHelper mSQLiteHelper;

    synchronized SQLite initialize(@NonNull final Context context, @NonNull final DatabaseArgs databaseArgs) {
        mSQLiteHelper = new SQLiteHelper(databaseArgs.echoSQL);

        final UpgradeCallback upgradeCallback = databaseArgs.upgradeCallback();
        final SQLiteDatabase.CursorFactory cursorFactory = databaseArgs.cursorFactory();

        mDatabaseHelper = new SQLiteOpenHelper(context,
                databaseArgs.databaseName,
                cursorFactory,
                databaseArgs.databaseVersion) {
            Map<Class<?>, String> cacheCreateTableSql = new HashMap<>();

            @Override
            public void onCreate(SQLiteDatabase db) {
                for (Class<?> tableClass : databaseArgs.tableClasses) {
                    String createTableSql = cacheCreateTableSql.get(tableClass);
                    if (createTableSql == null) {
                        createTableSql = SqlUtil.getCreateTableSql(tableClass);
                        cacheCreateTableSql.put(tableClass, createTableSql);
                    }
                    mSQLiteHelper.execSQL(db, createTableSql);
                }
            }

            @Override
            public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
                if (db.needUpgrade(newVersion)) {
                    onCreate(db);
                    if (upgradeCallback != null) {
                        String upgradeSQL = upgradeCallback.getUpgradeSQL(oldVersion, newVersion);
                        if (StringUtil.isNotEmpty(upgradeSQL)) {
                            mSQLiteHelper.execSQL(db, upgradeSQL);
                        }
                    }
                }
            }
        };
        return this;
    }

    @Override
    public <T> boolean exists(@NonNull T t) {
        Class<?> tableClass = t.getClass();
        ColumnInfo primaryKeyColumn = TableInfos.getPrimaryKeyColumn(tableClass);
        Object primaryKeyValue = null;
        try {
            Object value = primaryKeyColumn.getValue(t);
            if (value instanceof Date) {
                primaryKeyValue = ((Date) value).getTime();
            } else {
                primaryKeyValue = value;
            }
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        long count = count(tableClass,
                String.format("%s = ?", primaryKeyColumn.columnName),
                new String[]{String.valueOf(primaryKeyValue)});
        return count <= 0;
    }

    @Override
    public <T> boolean save(@NonNull T t) {
        return exists(t) ? insert(t) : update(t);
    }

    @Override
    public <T> boolean insert(@NonNull T t) {
        try {
            return insertOrThrow(t);
        } catch (SQLException e) {
            Log.d(TAG, "insert: ", e);
            return false;
        }
    }

    @Override
    public long insertInTx(@NonNull Collection<?> objects) {
        return insertInTx(objects, true);
    }

    @Override
    public long insertInTx(@NonNull Collection<?> objects, boolean errorContinue) {
        SQLiteDatabase database = getWritableDatabase();
        long count = 0;
        try {
            database.beginTransaction();
            for (Object object : objects) {
                boolean isSuccess = insert(object);
                if (isSuccess) {
                    count++;
                } else if (!errorContinue) {
                    return -1;
                }
            }
            if (count > 0) database.setTransactionSuccessful();
        } finally {
            database.endTransaction();
        }
        return count;
    }

    @Override
    public <T> boolean insertOrThrow(@NonNull T t) throws SQLException {
        Class<?> tableClass = t.getClass();
        TableInfo tableInfo = TableInfos.getTableInfo(tableClass);
        String tableName = tableInfo.tableName;
        ColumnInfo primaryKeyColumn = TableInfos.getPrimaryKeyColumn(tableClass);

        SQLiteDatabase database = getWritableDatabase();
        ContentValues values = getContentValues(t);
        // 如果主键被设置未自动增长则移除主键对应的值
        if (primaryKeyColumn.primaryKey != null && primaryKeyColumn.primaryKey.autoincrement()) {
            values.remove(primaryKeyColumn.columnName);
        }
        return mSQLiteHelper.insertOrThrow(database, tableName, null, values) != -1;
    }

    @Override
    public <T> boolean update(@NonNull T t) {
        Class<?> tableClass = t.getClass();
        TableInfo tableInfo = TableInfos.getTableInfo(tableClass);
        String tableName = tableInfo.tableName;
        ColumnInfo primaryKeyColumn = TableInfos.getPrimaryKeyColumn(tableClass);

        ContentValues values = getContentValues(t);
        Object primaryKeyValue = null;
        try {
            Object value = primaryKeyColumn.getValue(t);
            if (value instanceof Date) {
                primaryKeyValue = ((Date) value).getTime();
            } else {
                primaryKeyValue = value;
            }
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        SQLiteDatabase database = getWritableDatabase();
        try {
            int count = mSQLiteHelper.updateOrThrow(database, tableName, values,
                    String.format("%s = ?", primaryKeyColumn.columnName),
                    new String[]{String.valueOf(primaryKeyValue)});
            return count > 0;
        } catch (Exception e) {
            return false;
        }
    }

    @Override
    public long updateInTx(@NonNull Collection<?> objects) {
        return updateInTx(objects, true);
    }

    @Override
    public long updateInTx(@NonNull Collection<?> objects, boolean errorContinue) {
        SQLiteDatabase database = getWritableDatabase();
        long count = 0;
        try {
            database.beginTransaction();
            for (Object object : objects) {
                boolean isSuccess = update(object);
                if (isSuccess) {
                    count++;
                } else if (!errorContinue) {
                    return -1;
                }
            }
            if (count > 0) database.setTransactionSuccessful();
        } finally {
            database.endTransaction();
        }
        return count;
    }


    @Override
    public <T> long updateByCondition(@NonNull T t,
                                      @Nullable String whereClause,
                                      @Nullable String[] whereArgs) {

        Class<?> tableClass = t.getClass();
        TableInfo tableInfo = TableInfos.getTableInfo(tableClass);
        String tableName = tableInfo.tableName;

        ContentValues contentValues = getContentValues(t);
        return updateByCondition(tableName, contentValues, whereClause, whereArgs);
    }

    @Override
    public long updateByCondition(@NonNull Class<?> tableClass,
                                  @NonNull String[] columns,
                                  @NonNull Object[] values,
                                  @Nullable String whereClause,
                                  @Nullable String[] whereArgs) {
        if (columns.length != values.length) {
            throw new IllegalArgumentException("columns.length != values.length");
        }
        TableInfo tableInfo = TableInfos.getTableInfo(tableClass);
        String tableName = tableInfo.tableName;

        ContentValues contentValues = new ContentValues(columns.length);
        for (int i = 0; i < columns.length; i++) {
            SQLiteValueConverter.putToContentValues(contentValues, columns[i], values[i]);
        }
        return updateByCondition(tableName, contentValues, whereClause, whereArgs);
    }

    private long updateByCondition(@NonNull String tableName,
                                   @NonNull ContentValues contentValues,
                                   @Nullable String whereClause,
                                   @Nullable String[] whereArgs) {
        SQLiteDatabase database = getWritableDatabase();
        try {
            database.beginTransaction();
            int count = mSQLiteHelper.updateOrThrow(database, tableName, contentValues, whereClause, whereArgs);
            database.setTransactionSuccessful();
            return count;
        } catch (Exception e) {
            return 0;
        } finally {
            database.endTransaction();
        }
    }

    @Override
    public <T> boolean delete(@NonNull T t) {
        Class<?> tableClass = t.getClass();

        ColumnInfo primaryKeyColumn = TableInfos.getPrimaryKeyColumn(tableClass);
        Object primaryKeyValue = null;
        try {
            Object value = primaryKeyColumn.getValue(t);
            if (value instanceof Date) {
                primaryKeyValue = ((Date) value).getTime();
            } else {
                primaryKeyValue = value;
            }
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        String[] whereArgs = {String.valueOf(primaryKeyValue)};
        return deleteByCondition(tableClass,
                String.format("%s = ?", primaryKeyColumn.columnName), whereArgs) > 0;
    }

    @Override
    public int deleteByCondition(@NonNull Class<?> tableClass,
                                 @Nullable String whereClause,
                                 @Nullable String[] whereArgs) {
        TableInfo tableInfo = TableInfos.getTableInfo(tableClass);

        SQLiteDatabase database = getWritableDatabase();
        return mSQLiteHelper.delete(database, tableInfo.tableName, whereClause, whereArgs);
    }

    @Override
    public int deleteAll(@NonNull Class<?> tableClass) {
        TableInfo tableInfo = TableInfos.getTableInfo(tableClass);
        SQLiteDatabase database = getWritableDatabase();
        return mSQLiteHelper.delete(database, tableInfo.tableName, null, null);
    }

    @Nullable
    @Override
    public <T> T findOne(@NonNull Class<T> clazz) {
        return findOne(clazz, null, null);
    }

    @Nullable
    @Override
    public <T> T findOne(@NonNull Class<T> clazz,
                         @Nullable String whereClause,
                         @Nullable String[] whereArgs) {
        Iterator<T> iterator = findAsIterator(clazz, whereClause, whereArgs);
        try {
            while (iterator.hasNext()) {
                T t = iterator.next();
                if (t != null) {
                    return t;
                }
            }
            return null;
        } finally {
            iterator.remove();
        }
    }

    @Nullable
    @Override
    public <T> T findOne(@NonNull FindArgs<T> findArgs) {
        Iterator<T> iterator = findAsIterator(findArgs);
        try {
            while (iterator.hasNext()) {
                T t = iterator.next();
                if (t != null) {
                    return t;
                }
            }
            return null;
        } finally {
            iterator.remove();
        }
    }

    @NonNull
    @Override
    public <T> List<T> findAsList(@NonNull Class<T> clazz) {
        return findAsList(clazz, null, null);
    }

    @NonNull
    @Override
    public <T> List<T> findAsList(@NonNull Class<T> clazz,
                                  @Nullable String whereClause,
                                  @Nullable String[] whereArgs) {
        Iterator<T> iterator = findAsIterator(clazz, whereClause, whereArgs);
        List<T> list = new ArrayList<>();
        try {
            while (iterator.hasNext()) {
                T t = iterator.next();
                if (t != null) {
                    list.add(t);
                }
            }
            return list;
        } finally {
            iterator.remove();
        }
    }

    @NonNull
    @Override
    public <T> List<T> findAsList(@NonNull FindArgs<T> findArgs) {
        Iterator<T> iterator = findAsIterator(findArgs);
        List<T> list = new ArrayList<>();
        try {
            while (iterator.hasNext()) {
                T t = iterator.next();
                if (t != null) {
                    list.add(t);
                }
            }
            return list;
        } finally {
            iterator.remove();
        }
    }

    @NonNull
    @Override
    public <T> Iterator<T> findAsIterator(@NonNull Class<T> clazz) {
        return findAsIterator(clazz, null, null);
    }

    @NonNull
    @Override
    public <T> Iterator<T> findAsIterator(@NonNull Class<T> tableClass,
                                          @Nullable String whereClause,
                                          @Nullable String[] whereArgs) {
        TableInfo tableInfo = TableInfos.getTableInfo(tableClass);

        SQLiteDatabase database = getReadableDatabase();
        Cursor cursor = mSQLiteHelper.query(database, false, tableInfo.tableName, null,
                whereClause, whereArgs, null, null,
                null, null);

        return CursorIterator.create(cursor, tableClass, tableInfo.columnInfos);
    }

    @NonNull
    @Override
    public <T> Iterator<T> findAsIterator(@NonNull FindArgs<T> findArgs) {
        TableInfo tableInfo = TableInfos.getTableInfo(findArgs.tableClass);
        SQLiteDatabase database = getReadableDatabase();
        Cursor cursor = mSQLiteHelper.query(database, findArgs.isDistinct(),
                tableInfo.tableName, findArgs.getColumns(),
                findArgs.getWhereClause(), findArgs.getWhereArgs(),
                findArgs.getGroupBy(), findArgs.getHaving(),
                findArgs.getOrderBy(), findArgs.getLimit());
        return CursorIterator.create(cursor, findArgs.tableClass, tableInfo.columnInfos);
    }

    @Override
    public long count(@NonNull Class<?> clazz) {
        return count(clazz, null, null);
    }

    @Override
    public long count(@NonNull Class<?> tableClass, @Nullable String whereClause, @Nullable String[] whereArgs) {

        TableInfo tableInfo = TableInfos.getTableInfo(tableClass);
        ColumnInfo primaryKeyColumn = TableInfos.getPrimaryKeyColumn(tableClass);
        String[] columns = {"COUNT(" + primaryKeyColumn.columnName + ")"};
        return count(tableInfo.tableName, false, columns, whereClause, whereArgs);
    }

    @Override
    public long count(@NonNull CountArgs<?> countArgs) {
        TableInfo tableInfo = TableInfos.getTableInfo(countArgs.tableClass);
        ColumnInfo primaryKeyColumn = TableInfos.getPrimaryKeyColumn(countArgs.tableClass);

        String[] columns = {"COUNT(" + primaryKeyColumn.columnName + ")"};
        return count(tableInfo.tableName, countArgs.isDistinct(), columns,
                countArgs.getWhereClause(), countArgs.getWhereArgs());
    }

    private long count(@NonNull String tableName,
                       boolean distinct,
                       @NonNull String[] columns,
                       @Nullable String whereClause,
                       @Nullable String[] whereArgs) {
        SQLiteDatabase database = getReadableDatabase();
        Cursor cursor = null;
        long count = 0;
        try {
            cursor = mSQLiteHelper.query(database, distinct, tableName, columns,
                    whereClause, whereArgs, null, null, null, null);
            if (cursor.moveToFirst()) count = cursor.getLong(0);
            return count;
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }
    }


    @Override
    public synchronized void destroy() {
        if (mWritableDatabase != null) {
            if (mWritableDatabase.isOpen()) {
                mWritableDatabase.close();
            }
            mWritableDatabase = null;
        }
        if (mReadableDatabase != null) {
            if (mReadableDatabase.isOpen()) {
                mReadableDatabase.close();
            }
            mReadableDatabase = null;
        }
        if (mDatabaseHelper != null) {
            mDatabaseHelper.close();
            mDatabaseHelper = null;
        }
    }

    @NonNull
    @Override
    public synchronized SQLiteDatabase getWritableDatabase() {
        if (mWritableDatabase != null && mWritableDatabase.isOpen()) {
            return mWritableDatabase;
        }
        return mWritableDatabase = mDatabaseHelper.getWritableDatabase();
    }

    @NonNull
    @Override
    public synchronized SQLiteDatabase getReadableDatabase() {
        if (mReadableDatabase != null && mReadableDatabase.isOpen()) {
            return mReadableDatabase;
        }
        return mReadableDatabase = mDatabaseHelper.getReadableDatabase();
    }


    @NonNull
    private <T> ContentValues getContentValues(T t) {
        Class<?> tableClass = t.getClass();
        TableInfo tableInfo = TableInfos.getTableInfo(tableClass);

        ContentValues values = new ContentValues(tableInfo.columnInfos.size());
        for (ColumnInfo columnInfo : tableInfo.columnInfos) {
            SQLiteValueConverter.putToContentValues(values, columnInfo, t);
        }
        return values;
    }


}
